﻿using System;
using System.Linq.Expressions;
using System.Reflection;

namespace MyBasicLib.Reflection.FastReflection.Lambda
{
    public class PropertyAccessor : IPropertyAccessor
    {
        private Func<object, object> m_getter;
        private IMethodInvoker m_setMethodInvoker;

        public PropertyAccessor(PropertyInfo propertyInfo)
        {
            PropertyInfo = propertyInfo;
            InitializeGet(propertyInfo);
            InitializeSet(propertyInfo);
        }

        public PropertyInfo PropertyInfo { get; private set; }

        #region IPropertyAccessor Members

        object IPropertyAccessor.GetValue(object instance)
        {
            return GetValue(instance);
        }

        void IPropertyAccessor.SetValue(object instance, object value)
        {
            SetValue(instance, value);
        }

        #endregion

        private void InitializeGet(PropertyInfo propertyInfo)
        {
            if (!propertyInfo.CanRead) return;

            // Target: (object)(((TInstance)instance).Property)

            // preparing parameter, object type
            ParameterExpression instance = Expression.Parameter(typeof (object), "instance");

            // non-instance for static method, or ((TInstance)instance)
            UnaryExpression instanceCast = propertyInfo.GetGetMethod(true).IsStatic
                                               ? null
                                               : Expression.Convert(instance, propertyInfo.ReflectedType);

            // ((TInstance)instance).Property
            MemberExpression propertyAccess = Expression.Property(instanceCast, propertyInfo);

            // (object)(((TInstance)instance).Property)
            UnaryExpression castPropertyValue = Expression.Convert(propertyAccess, typeof (object));

            // Lambda expression
            Expression<Func<object, object>> lambda = Expression.Lambda<Func<object, object>>(castPropertyValue,
                                                                                              instance);

            m_getter = lambda.Compile();
        }

        private void InitializeSet(PropertyInfo propertyInfo)
        {
            if (!propertyInfo.CanWrite) return;
            MethodInfo methodInfo = propertyInfo.GetSetMethod(true);
            // this.m_setMethodInvoker = Caches.Get(methodInfo, p => new MethodInvoker(p));
            m_setMethodInvoker = FastReflectionCaches.Get(methodInfo,
                                                          p => new MethodInvoker(p));
        }

        public object GetValue(object o)
        {
            if (m_getter == null)
            {
                throw new NotSupportedException("Get method is not defined for this property.");
            }

            return m_getter(o);
        }

        public void SetValue(object o, object value)
        {
            if (m_setMethodInvoker == null)
            {
                throw new NotSupportedException("Set method is not defined for this property.");
            }

            m_setMethodInvoker.Invoke(o, new[] {value});
        }
    }
}